4.06. Ресурсопотребление и метрики
Ресурсопотребление и метрики
Ресурсы
Ресурсы вычислительной системы — это ограниченные компоненты, необходимые для выполнения программ.
Основные категории ресурсов:
- процессорное время (CPU time)
- оперативная память (RAM)
- дисковое пространство и операции ввода-вывода
- сетевые ресурсы
- специализированные ресурсы (GPU, TPU)
Ресурсы делятся на:
- вычислительные — процессор, сопроцессоры
- хранилища — оперативная память, дисковое пространство
- коммуникационные — сетевые интерфейсы, шины данных
Что измерять — CPU, память, дисковый I/O, сеть
Метрики производительности — количественные показатели использования ресурсов системы.
| Ресурс | Ключевые метрики | Единицы измерения |
|---|---|---|
| CPU | Использование, время в пользовательском/системном режиме, контекстные переключения | проценты, миллисекунды |
| Память | Используемая память, выделенная память, свопинг, частота сборки мусора | мегабайты, гигабайты |
| Диск | Операции ввода-вывода в секунду (IOPS), пропускная способность, задержка | операции/сек, МБ/с, мс |
| Сеть | Пропускная способность, задержка, количество пакетов, ошибки | Мбит/с, мс, пакеты/с |
CPU — на что обращать внимание
Процессорное время — основной ресурс для выполнения инструкций программы.
Важные аспекты анализа CPU:
-
Утилизация процессора
- 0-70% — нормальная нагрузка
- 70-90% — высокая нагрузка, возможны задержки
- 90-100% — критическая нагрузка, риск деградации производительности
-
Режимы выполнения
- Пользовательский режим — выполнение кода приложения
- Системный режим — выполнение системных вызовов ядром ОС Высокий процент системного времени может указывать на чрезмерное количество системных вызовов
-
Контекстные переключения Частые переключения между потоками создают накладные расходы. Норма — до нескольких тысяч переключений в секунду на ядро.
-
Прерывания Аппаратные прерывания от устройств могут конкурировать за процессорное время.
Пример анализа в Linux:
# Просмотр загрузки CPU
top
# Детальная статистика
vmstat 1
# Процент времени в разных режимах
mpstat -P ALL 1
Пример анализа в Windows через PowerShell:
# Загрузка CPU по процессам
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, CPU
# Системная статистика
Get-Counter '\Processor(_Total)\% Processor Time'
Память — на что обращать внимание
Оперативная память — временно хранилище данных и кода во время выполнения программы.
Ключевые метрики памяти:
-
Используемая память Объём памяти, выделенный процессом. Включает:
- кучу (heap) — динамически выделяемая память
- стек (stack) — память для локальных переменных и вызовов
- сегмент кода — исполняемые инструкции
- сегмент данных — глобальные и статические переменные
-
Резидентная память (RSS) Фактически загруженная в физическую память часть процесса.
-
Виртуальная память Общий адресное пространство процесса, включая своп и зарезервированные области.
-
Свопинг Перемещение страниц памяти между оперативной памятью и диском. Частый свопинг указывает на нехватку оперативной памяти.
-
Утечки памяти Постепенный рост используемой памяти без освобождения.
Пример обнаружения утечки памяти в C#:
public class MemoryLeakExample
{
private static readonly List<byte[]> _cache = new List<byte[]>();
public void AddToCache()
{
// Добавляем данные в кэш, но никогда не удаляем старые
_cache.Add(new byte[10 * 1024 * 1024]); // 10 МБ
}
// Правильный вариант с ограничением размера кэша
public void AddToCacheWithLimit()
{
_cache.Add(new byte[10 * 1024 * 1024]);
if (_cache.Count > 100)
{
_cache.RemoveRange(0, _cache.Count - 100); // Оставляем последние 100 элементов
}
}
}
Пример мониторинга памяти в Python:
import tracemalloc
import gc
# Включение трассировки выделения памяти
tracemalloc.start()
# Выполнение кода
# ...
# Снимок текущего состояния
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 10 memory allocations ]")
for stat in top_stats[:10]:
print(stat)
# Принудительная сборка мусора
gc.collect()
Диск — на что обращать внимание
Дисковая подсистема — критический ресурс для операций ввода-вывода.
Важные метрики дисковой подсистемы:
-
IOPS (Input/Output Operations Per Second) Количество операций чтения/записи в секунду. Зависит от типа носителя:
- HDD: 50-200 IOPS
- SATA SSD: 10 000-100 000 IOPS
- NVMe SSD: 100 000-1 000 000+ IOPS
-
Пропускная способность Объём данных, передаваемых в секунду (МБ/с или ГБ/с).
-
Задержка (latency) Время от начала операции до её завершения:
- хороший показатель: < 10 мс
- приемлемый: 10-50 мс
- проблемный: > 50 мс
-
Очереди операций Накопление запросов на диск указывает на перегрузку подсистемы.
Пример анализа дисковой активности в Linux:
# Мониторинг в реальном времени
iostat -x 1
# Детальная статистика по устройствам
iotop
# Просмотр очередей
cat /proc/diskstats
Пример оптимизации дисковых операций:
// Плохо: множественные мелкие записи
using (var writer = new StreamWriter("log.txt"))
{
foreach (var item in items)
{
writer.WriteLine(item.ToString()); // Отдельная операция ввода-вывода
}
}
// Хорошо: пакетная запись с буферизацией
using (var writer = new StreamWriter("log.txt", bufferSize: 8192))
{
var batch = new StringBuilder();
foreach (var item in items)
{
batch.AppendLine(item.ToString());
if (batch.Length > 8192)
{
writer.Write(batch.ToString());
batch.Clear();
}
}
writer.Write(batch.ToString());
}
Сеть — на что обращать внимание
Сетевые ресурсы — каналы передачи данных между системами.
Ключевые сетевые метрики:
-
Пропускная способность Максимальный объём данных, передаваемых по сети в единицу времени.
-
Задержка (latency) Время прохождения пакета от отправителя к получателю:
- локальная сеть: 0.1-1 мс
- городская сеть: 1-10 мс
- межконтинентальная: 50-200 мс
-
Джиттер (jitter) Вариативность задержки. Высокий джиттер проблематичен для реального времени.
-
Потери пакетов Процент пакетов, не достигших получателя. Приемлемый уровень — < 1%.
-
Количество соединений Ограничение на количество одновременных TCP-соединений.
Пример мониторинга сети в Linux:
# Статистика сетевых интерфейсов
iftop
# Детальная статистика
nload
# Просмотр активных соединений
netstat -an | grep ESTABLISHED | wc -l
Пример оптимизации сетевых вызовов:
// Плохо: множество мелких запросов
public async Task<List<User>> GetUsersBad(List<int> userIds)
{
var users = new List<User>();
foreach (var id in userIds)
{
var user = await _httpClient.GetAsync($"api/users/{id}");
users.Add(await user.Content.ReadAsAsync<User>());
}
return users;
}
// Хорошо: пакетный запрос
public async Task<List<User>> GetUsersGood(List<int> userIds)
{
var response = await _httpClient.PostAsJsonAsync(
"api/users/batch",
new { userIds }
);
return await response.Content.ReadAsAsync<List<User>>();
}
Метрики кода — cyclomatic complexity, cognitive complexity, coupling, cohesion
Метрики качества кода — количественные показатели структуры и сложности программного кода.
| Метрика | Описание | Идеальное значение |
|---|---|---|
| Цикломатическая сложность | Количество линейно независимых путей выполнения | < 10 на метод |
| Когнитивная сложность | Сложность понимания кода человеком | < 15 на метод |
| Связность (coupling) | Степень зависимости между модулями | Минимальная |
| Связность (cohesion) | Степень объединения функциональности внутри модуля | Максимальная |
| Глубина наследования | Количество уровней в иерархии наследования | < 5 |
| Количество параметров | Аргументы метода | < 5 |
Пример высокой цикломатической сложности:
// Цикломатическая сложность: 8 (плохо)
public decimal CalculateDiscount(Order order, Customer customer, DateTime now)
{
if (customer.IsPremium)
{
if (order.Total > 1000)
return order.Total * 0.2m;
else
return order.Total * 0.15m;
}
else
{
if (order.Total > 1000)
{
if (now.DayOfWeek == DayOfWeek.Monday)
return order.Total * 0.1m;
else
return order.Total * 0.05m;
}
else
{
return 0;
}
}
}
Рефакторинг для снижения сложности:
// Цикломатическая сложность: 2 (хорошо)
public decimal CalculateDiscount(Order order, Customer customer, DateTime now)
{
var baseDiscount = GetBaseDiscount(customer, order.Total);
var dayBonus = GetDayBonus(now, order.Total);
return order.Total * (baseDiscount + dayBonus);
}
private decimal GetBaseDiscount(Customer customer, decimal total)
{
if (!customer.IsPremium) return 0;
return total > 1000 ? 0.15m : 0.1m;
}
private decimal GetDayBonus(DateTime now, decimal total)
{
if (total <= 1000) return 0;
return now.DayOfWeek == DayOfWeek.Monday ? 0.05m : 0;
}
Профилировщики: CPU profiling, memory profiling, allocation tracking
Профилировщик — инструмент для измерения производительности и потребления ресурсов программой.
Типы профилировщиков:
-
Сэмплинговые профилировщики Периодически снимают состояние стека вызовов. Низкие накладные расходы, но могут пропустить короткие вызовы.
-
Инструментирующие профилировщики Вставляют код измерения в каждый метод. Точны, но создают значительные накладные расходы.
-
Трассировочные профилировщики Записывают последовательность событий выполнения. Подходят для анализа временных зависимостей.
Пример использования профилировщика памяти в .NET:
using System.Diagnostics;
// Создание снимка памяти
var snapshot1 = Process.GetCurrentProcess().WorkingSet64;
// Выполнение кода
ProcessData();
// Второй снимок
var snapshot2 = Process.GetCurrentProcess().WorkingSet64;
Console.WriteLine($"Использовано памяти: {(snapshot2 - snapshot1) / 1024 / 1024} МБ");
Пример использования профилировщика в Java (VisualVM):
// Запуск приложения с агентом профилирования
java -agentpath:/path/to/libprofiler.so=cpu=sample,alloc=5m MyApplication
// Или подключение к работающему процессу через JMX
Разбивка по стеку: attribution of resource usage to call paths
Атрибуция ресурсов по стеку вызовов — распределение потребления ресурсов между различными путями вызовов в программе.
Пример атрибуции памяти:
Выделено 100 МБ:
├─ ProcessOrders() — 60 МБ (60%)
│ ├─ ValidateOrder() — 10 МБ (10%)
│ ├─ CalculateTotal() — 30 МБ (30%)
│ │ └─ ApplyDiscounts() — 25 МБ (25%)
│ └─ SaveOrder() — 20 МБ (20%)
└─ GenerateReport() — 40 МБ (40%)
└─ FormatData() — 35 МБ (35%)
Инструменты для атрибуции:
- async-profiler (JVM) — атрибуция по стеку с низкими накладными расходами
- eBPF (Linux) — системная атрибуция без модификации приложения
- Windows Performance Toolkit — детальная атрибуция для Windows
Пример использования eBPF для атрибуции памяти:
# Трассировка выделений памяти с привязкой к стеку вызовов
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc { @[ustack()] = count(); }'
Бюджеты производительности: SLA, latency targets
Бюджет производительности — количественное ограничение на время выполнения операции или потребление ресурсов.
Типы бюджетов:
-
Временные бюджеты
- время отклика (latency): 95-й перцентиль < 200 мс
- время загрузки страницы: < 3 секунды
- время запуска приложения: < 5 секунд
-
Ресурсные бюджеты
- потребление памяти: < 500 МБ на процесс
- использование CPU: < 30% в среднем
- размер пакета: < 2 МБ
-
Сетевые бюджеты
- количество запросов: < 10 на страницу
- общий размер ресурсов: < 1.5 МБ
Пример определения бюджета в конфигурации:
{
"performanceBudget": {
"timeToInteractive": 3500,
"firstContentfulPaint": 1800,
"largestContentfulPaint": 2500,
"cumulativeLayoutShift": 0.1,
"totalBlockingTime": 200,
"resourceSizes": {
"total": 1572864,
"scripts": 524288,
"images": 786432
}
}
}
Пример мониторинга бюджета в коде:
public class PerformanceMonitor
{
private readonly Stopwatch _stopwatch = new Stopwatch();
private const long LatencyBudgetMs = 200;
public async Task<T> ExecuteWithMonitoring<T>(Func<Task<T>> operation)
{
_stopwatch.Restart();
var result = await operation();
_stopwatch.Stop();
var latency = _stopwatch.ElapsedMilliseconds;
if (latency > LatencyBudgetMs)
{
_logger.LogWarning(
"Превышен бюджет задержки: {Latency}мс > {Budget}мс",
latency,
LatencyBudgetMs
);
}
return result;
}
}